﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Funq;
using NUnit.Framework;
using ServiceStack.Data;
using ServiceStack.OrmLite;
using ServiceStack.Redis;
using ServiceStack.Text;
using ServiceStack.Web;

namespace ServiceStack.WebHost.Endpoints.Tests;

public class ReplyAllAppHost() : AppSelfHostBase(nameof(ReplyAllTests), typeof(ReplyAllService).Assembly)
{
    public override void Configure(Container container)
    {
        GlobalRequestFilters.Add((rew, res, dto) =>
            ReplyAllRequestAttribute.AssertSingleDto(dto));

        GlobalResponseFilters.Add((rew, res, dto) =>
            ReplyAllResponseAttribute.AssertSingleDto(dto));

        container.Register<IDbConnectionFactory>(c =>
            new OrmLiteConnectionFactory(":memory:", SqliteDialect.Provider));

        container.Register<IRedisClientsManager>(c =>
            new RedisManagerPool());
    }
}

public class ReplyAllRequestAttribute : RequestFilterAttribute
{
    public override void Execute(IRequest req, IResponse res, object requestDto)
    {
        AssertSingleDto(requestDto);
    }

    public static void AssertSingleDto(object dto)
    {
        if (!(dto is BatchThrows || dto is BatchThrowsAsync || dto is NoRepeat || dto is HelloAll || dto is HelloAllAsync || dto is HelloAllVoid || dto is HelloAllVoidAsync || dto is HelloGet || dto is HelloAllCustom || dto is HelloAllTransaction || dto is Request))
            throw new Exception("Invalid " + dto.GetType().Name);
    }
}

public class ReplyAllResponseAttribute : ResponseFilterAttribute
{
    public override void Execute(IRequest req, IResponse res, object responseDto)
    {
        AssertSingleDto(responseDto);
    }

    public static void AssertSingleDto(object dto)
    {
        if (!(dto == null || dto is Task || dto is BatchThrowsResponse || dto is NoRepeatResponse || dto is HelloAllResponse || dto is HelloAllCustomResponse
              || dto is HelloAllTransactionResponse || dto is IHttpResult))
            throw new Exception("Invalid " + dto.GetType().Name);
    }
}

public class ReplyAllArrayRequestAttribute : RequestFilterAttribute
{
    public override void Execute(IRequest req, IResponse res, object requestDto)
    {
        if (requestDto.GetType() != typeof(HelloAllCustom[]))
            throw new Exception("Invalid " + requestDto.GetType().Name);
    }
}

public class ReplyAllArrayResponseAttribute : ResponseFilterAttribute
{
    public override void Execute(IRequest req, IResponse res, object responseDto)
    {
        //still based on Response of Service
        if (responseDto.GetType() != typeof(List<HelloAllCustomResponse>))
            throw new Exception("Invalid " + responseDto.GetType().Name);
    }
}

public class HelloAll : IReturn<HelloAllResponse>
{
    public string Name { get; set; }
}

public class HelloAllAsync : IReturn<HelloAllResponse>
{
    public string Name { get; set; }
}

public class HelloAllVoid : IReturnVoid
{
    public static int Counter;

    public string Name { get; set; }
}

public class HelloAllVoidAsync : IReturnVoid
{
    public static int Counter;

    public string Name { get; set; }
}

public class HelloGet : IReturn<HelloAllResponse>
{
    public string Name { get; set; }
}

public class HelloAllResponse
{
    public string Result { get; set; }
}

public class HelloAllCustom : IReturn<HelloAllCustomResponse>
{
    public string Name { get; set; }
}

public class HelloAllCustomResponse
{
    public string Result { get; set; }
}

public class HelloAllTransaction : IReturn<HelloAllTransactionResponse>
{
    public string Name { get; set; }
}

public class HelloAllTransactionResponse
{
    public string Result { get; set; }
}

public class Request : IReturnVoid
{
    public int Id { get; set; }
    public string Name { get; set; }
}

public class ReplyAllService : Service
{
    public static int TimesExecuted = 0;

    [ReplyAllRequest]
    [ReplyAllResponse]
    public object Any(HelloAll request)
    {
        TimesExecuted++;
        return new HelloAllResponse { Result = "Hello, {0}!".Fmt(request.Name) };
    }

    [ReplyAllRequest]
    [ReplyAllResponse]
    public object Any(HelloAllAsync request)
    {
        return Task.FromResult(new HelloAllResponse { Result = "Hello, {0}!".Fmt(request.Name) });
    }

    public object Get(HelloGet request)
    {
        return new HelloAllResponse { Result = "Hello, {0}!".Fmt(request.Name) };
    }

    public void Any(HelloAllVoid request)
    {
        HelloAllVoid.Counter++;
    }

    public async Task Any(HelloAllVoidAsync request)
    {
        HelloAllVoidAsync.Counter++;
        await Task.FromResult(0);
    }

    [ReplyAllRequest]
    [ReplyAllResponse]
    public object Any(HelloAllCustom request)
    {
        return new HelloAllCustomResponse { Result = "Hello, {0}!".Fmt(request.Name) };
    }

    [ReplyAllArrayRequest]
    [ReplyAllArrayResponse]
    public object Any(HelloAllCustom[] requests)
    {
        return requests.Map(x => new HelloAllCustomResponse
        {
            Result = "Custom, {0}!".Fmt(x.Name)
        });
    }

    public object Any(HelloAllTransaction request)
    {
        if (request.Name == "Bar")
            throw new ArgumentException("No Bar allowed here");

        Db.Insert(request);

        return new HelloAllTransactionResponse { Result = "Hello, {0}!".Fmt(request.Name) };
    }

    public object Any(HelloAllTransaction[] requests)
    {
        using (var trans = Db.OpenTransaction())
        {
            var response = requests.Map(Any);

            trans.Commit();

            return response;
        }
    }

    public void Any(Request request)
    {
        Redis.Store(request);
    }

    public void Any(Request[] requests)
    {
        Redis.StoreAll(requests);
    }
}

public class NoRepeat : IReturn<NoRepeatResponse>
{
    public Guid Id { get; set; }
}

public class NoRepeatResponse
{
    public Guid Id { get; set; }
}

public class BatchThrows : IReturn<BatchThrowsResponse>
{
    public int Id { get; set; }
    public string Name { get; set; }
}

public class BatchThrowsAsync : IReturn<BatchThrowsResponse>
{
    public int Id { get; set; }
    public string Name { get; set; }
}

public class BatchThrowsResponse
{
    public string Result { get; set; }

    public ResponseStatus ResponseStatus { get; set; }
}

public class AutoBatchServices : IService
{
    private static readonly HashSet<Guid> ReceivedGuids = new HashSet<Guid>();

    public NoRepeatResponse Any(NoRepeat request)
    {
        if (ReceivedGuids.Contains(request.Id))
            throw new ArgumentException("Id {0} already received".Fmt(request.Id));

        ReceivedGuids.Add(request.Id);

        return new NoRepeatResponse
        {
            Id = request.Id
        };
    }

    public object Any(BatchThrows request)
    {
        throw new Exception("Batch Throws");
    }

    public async Task Any(BatchThrowsAsync request)
    {
        await Task.Delay(0);

        throw new Exception("Batch Throws");
    }
}

public class ReplyAllJsonServiceClientTests : ReplyAllTests
{
    public override IServiceClient CreateClient(string baseUri)
    {
        return new JsonServiceClient(baseUri);
    }

    public override IServiceClientAsync CreateClientAsync(string baseUri)
    {
        return new JsonServiceClient(baseUri);
    }
}

public class ReplyAllJsonHttpClientTests : ReplyAllTests
{
    public override IServiceClient CreateClient(string baseUri)
    {
        return new JsonHttpClient(baseUri);
    }

    public override IServiceClientAsync CreateClientAsync(string baseUri)
    {
        return new JsonHttpClient(baseUri);
    }
}

public class ReplyAllXmlServiceClientTests : ReplyAllTests
{
    public override IServiceClient CreateClient(string baseUri)
    {
        return new XmlServiceClient(baseUri);
    }

    public override IServiceClientAsync CreateClientAsync(string baseUri)
    {
        return new XmlServiceClient(baseUri);
    }
}

public class ReplyAllCsvServiceClientTests : ReplyAllTests
{
    public override IServiceClient CreateClient(string baseUri)
    {
        return new CsvServiceClient(baseUri);
    }

    public override IServiceClientAsync CreateClientAsync(string baseUri)
    {
        return new CsvServiceClient(baseUri);
    }
}

[TestFixture]
public abstract class ReplyAllTests
{
    private ServiceStackHost appHost;

    [OneTimeSetUp]
    public void TestFixtureSetUp()
    {
        appHost = new ReplyAllAppHost()
            .Init()
            .Start(Config.AbsoluteBaseUri);
    }

    [OneTimeTearDown]
    public void TestFixtureTearDown()
    {
        appHost.Dispose();
    }

    public abstract IServiceClient CreateClient(string baseUri);
    public abstract IServiceClientAsync CreateClientAsync(string baseUri);

    [Test]
    public void Can_send_single_HelloAll_request()
    {
        var client = CreateClient(Config.AbsoluteBaseUri);

        var request = new HelloAll { Name = "Foo" };
        var response = client.Send(request);
        Assert.That(response.Result, Is.EqualTo("Hello, Foo!"));
    }

    [Test]
    public async Task Can_send_single_HelloAllAsync_request()
    {
        var client = CreateClientAsync(Config.AbsoluteBaseUri);

        var request = new HelloAllAsync { Name = "Foo" };
        var response = await client.SendAsync<HelloAllResponse>(request);
        Assert.That(response.Result, Is.EqualTo("Hello, Foo!"));
    }

    [Test]
    public void Can_send_multi_reply_HelloAll_requests()
    {
        ReplyAllService.TimesExecuted = 0;

        var client = CreateClient(Config.AbsoluteBaseUri);

        var requests = new[]
        {
            new HelloAll { Name = "Foo" },
            new HelloAll { Name = "Bar" },
            new HelloAll { Name = "Baz" },
        };

        var responses = client.SendAll(requests);
        var results = responses.Map(x => x.Result);

        Assert.That(results, Is.EquivalentTo(new[] {
            "Hello, Foo!", "Hello, Bar!", "Hello, Baz!"
        }));

        Assert.That(ReplyAllService.TimesExecuted, Is.EqualTo(requests.Length));
    }

    [Test]
    public async Task Can_send_multi_reply_HelloAllAsync_requests()
    {
        var client = CreateClient(Config.AbsoluteBaseUri);

        var requests = new[]
        {
            new HelloAllAsync { Name = "Foo" },
            new HelloAllAsync { Name = "Bar" },
            new HelloAllAsync { Name = "Baz" },
        };

        var responses = await client.SendAllAsync(requests);
        var results = responses.Map(x => x.Result);

        Assert.That(results, Is.EquivalentTo(new[] {
            "Hello, Foo!", "Hello, Bar!", "Hello, Baz!"
        }));
    }

    [Test]
    public void Can_send_multi_reply_HelloGet_requests()
    {
        var client = new JsonServiceClient(Config.AbsoluteBaseUri)
        {
            RequestFilter = req =>
                req.Headers[HttpHeaders.XHttpMethodOverride] = HttpMethods.Get
        };

        var requests = new[]
        {
            new HelloGet { Name = "Foo" },
            new HelloGet { Name = "Bar" },
            new HelloGet { Name = "Baz" },
        };

        client.Get(new HelloGet { Name = "aaa" });

        var responses = client.SendAll(requests);
        var results = responses.Map(x => x.Result);

        Assert.That(results, Is.EquivalentTo(new[] {
            "Hello, Foo!", "Hello, Bar!", "Hello, Baz!"
        }));
    }

    [Test]
    public void Can_send_multi_HelloAllVoid()
    {
        var client = CreateClient(Config.AbsoluteBaseUri);

        var requests = new[]
        {
            new HelloAllVoid { Name = "Foo" },
            new HelloAllVoid { Name = "Bar" },
            new HelloAllVoid { Name = "Baz" },
        };

        client.SendAllOneWay(requests);
    }

    [Test]
    public void Can_send_PublishAll_HelloAllVoid()
    {
        var client = CreateClient(Config.AbsoluteBaseUri);

        var requests = new[]
        {
            new HelloAllVoid { Name = "Foo" },
            new HelloAllVoid { Name = "Bar" },
            new HelloAllVoid { Name = "Baz" },
        };

        client.PublishAll(requests);
    }

    [Test]
    public void Can_send_multi_HelloAllVoidAsync()
    {
        var client = CreateClient(Config.AbsoluteBaseUri);

        var requests = new[]
        {
            new HelloAllVoidAsync { Name = "Foo" },
            new HelloAllVoidAsync { Name = "Bar" },
            new HelloAllVoidAsync { Name = "Baz" },
        };

        client.SendAllOneWay(requests);
    }

    [Test]
    public async Task Can_send_PublishAllAsync_HelloAllVoidAsync()
    {
        var client = CreateClient(Config.AbsoluteBaseUri);

        var requests = new[]
        {
            new HelloAllVoidAsync { Name = "Foo" },
            new HelloAllVoidAsync { Name = "Bar" },
            new HelloAllVoidAsync { Name = "Baz" },
        };

        await client.PublishAllAsync(requests);
    }

    [Test]
    public void Can_send_single_HelloAllCustom_request()
    {
        var client = CreateClient(Config.AbsoluteBaseUri);

        var request = new HelloAllCustom { Name = "Foo" };
        var response = client.Send(request);
        Assert.That(response.Result, Is.EqualTo("Hello, Foo!"));
    }

    [Test]
    public void Can_send_multi_reply_HelloAllCustom_requests()
    {
        var client = CreateClient(Config.AbsoluteBaseUri);

        var requests = new[]
        {
            new HelloAllCustom { Name = "Foo" },
            new HelloAllCustom { Name = "Bar" },
            new HelloAllCustom { Name = "Baz" },
        };

        var responses = client.SendAll(requests);
        var results = responses.Map(x => x.Result);

        Assert.That(results, Is.EquivalentTo(new[] {
            "Custom, Foo!", "Custom, Bar!", "Custom, Baz!"
        }));
    }

    [Test]
    public async Task Can_send_async_multi_reply_HelloAllCustom_requests()
    {
        var client = CreateClient(Config.AbsoluteBaseUri);

        var requests = new[]
        {
            new HelloAllCustom { Name = "Foo" },
            new HelloAllCustom { Name = "Bar" },
            new HelloAllCustom { Name = "Baz" },
        };

        var responses = await client.SendAllAsync(requests);
        var results = responses.Map(x => x.Result);

        Assert.That(results, Is.EquivalentTo(new[] {
            "Custom, Foo!", "Custom, Bar!", "Custom, Baz!"
        }));
    }

    [Test]
    public void Can_send_multi_oneway_HelloAllCustom_requests()
    {
        var client = CreateClient(Config.AbsoluteBaseUri);

        var requests = new[]
        {
            new HelloAllCustom { Name = "Foo" },
            new HelloAllCustom { Name = "Bar" },
            new HelloAllCustom { Name = "Baz" },
        };

        client.SendAllOneWay(requests);
    }

    [Test]
    public void Can_send_multiple_single_HelloAllTransaction()
    {
        using (var db = appHost.Resolve<IDbConnectionFactory>().Open())
        {
            db.DropAndCreateTable<HelloAllTransaction>();
        }

        var client = CreateClient(Config.AbsoluteBaseUri);

        var names = new[] { "Foo", "Bar", "Baz" };

        try
        {
            foreach (var name in names)
            {
                client.Send(new HelloAllTransaction { Name = name });
            }

            Assert.Fail("Should throw on Bar");
        }
        catch (WebServiceException ex)
        {
            Assert.That(ex.ErrorCode, Is.EqualTo(nameof(ArgumentException)));
        }

        using (var db = appHost.Resolve<IDbConnectionFactory>().Open())
        {
            var allRequests = db.Select<HelloAllTransaction>();
            Assert.That(allRequests.Count, Is.EqualTo(1));
            Assert.That(allRequests[0].Name, Is.EqualTo("Foo"));
        }
    }

    [Test]
    public void Sending_multiple_HelloAllTransaction_does_rollback_transaction()
    {
        using (var db = appHost.Resolve<IDbConnectionFactory>().Open())
        {
            db.DropAndCreateTable<HelloAllTransaction>();
        }

        var client = CreateClient(Config.AbsoluteBaseUri);
        var requests = new[]
        {
            new HelloAllTransaction { Name = "Foo" },
            new HelloAllTransaction { Name = "Bar" },
            new HelloAllTransaction { Name = "Baz" },
        };

        try
        {
            var responses = client.SendAll(requests);

            Assert.Fail("Should throw on Bar");
        }
        catch (WebServiceException ex)
        {
            Assert.That(ex.StatusDescription, Is.EqualTo(nameof(ArgumentException)));
        }

        using (var db = appHost.Resolve<IDbConnectionFactory>().Open())
        {
            var allRequests = db.Select<HelloAllTransaction>();
            Assert.That(allRequests.Count, Is.EqualTo(0));
        }
    }

    [Test]
    public void Can_store_multiple_requests_with_SendAllOneWay()
    {
        using (var redis = appHost.Resolve<IRedisClientsManager>().GetClient())
        {
            redis.FlushAll();

            var client = CreateClient(Config.AbsoluteBaseUri);
            var requests = new[]
            {
                new Request { Id = 1, Name = "Foo" },
                new Request { Id = 2, Name = "Bar" },
                new Request { Id = 3, Name = "Baz" },
            };

            client.SendAllOneWay(requests);

            var savedRequests = redis.As<Request>().GetAll();

            Assert.That(savedRequests.Map(x => x.Name), Is.EquivalentTo(new[] {
                "Foo", "Bar", "Baz"
            }));
        }
    }

    [Test]
    public void Does_not_repeat()
    {
        var client = CreateClient(Config.AbsoluteBaseUri);
        var batch = new[] { new NoRepeat { Id = Guid.NewGuid() }, new NoRepeat { Id = Guid.NewGuid() } };

        var results = client.SendAll(batch);
        var guids = results.Select(r => r.Id);
        Assert.IsTrue(guids.SequenceEqual(batch.Select(b => b.Id)));
    }

    [Test]
    public void Does_throw_WebServiceException_on_Error()
    {
        var client = CreateClient(Config.AbsoluteBaseUri);

        var requests = new[]
        {
            new BatchThrows { Id = 1, Name = "Foo" },
            new BatchThrows { Id = 2, Name = "Bar" },
            new BatchThrows { Id = 3, Name = "Baz" },
        };

        try
        {
            var responses = client.SendAll(requests);
            Assert.Fail("Should throw");
        }
        catch (WebServiceException ex)
        {
            Assert.That(ex.ErrorCode, Is.EqualTo(typeof(Exception).Name));
            Assert.That(ex.ResponseStatus.ErrorCode, Is.EqualTo(typeof(Exception).Name));
            Assert.That(ex.ResponseStatus.Message, Is.EqualTo("Batch Throws"));
            Assert.That(ex.ResponseHeaders[HttpHeaders.XAutoBatchCompleted], Is.EqualTo("0"));
        }
    }

    [Test]
    public async Task Does_throw_WebServiceException_on_Error_Async()
    {
        var client = CreateClient(Config.AbsoluteBaseUri);

        var requests = new[]
        {
            new BatchThrowsAsync { Id = 1, Name = "Foo" },
            new BatchThrowsAsync { Id = 2, Name = "Bar" },
            new BatchThrowsAsync { Id = 3, Name = "Baz" },
        };

        try
        {
            var responses = await client.SendAllAsync(requests);
            Assert.Fail("Should throw");
        }
        catch (WebServiceException ex)
        {
            Assert.That(ex.ErrorCode, Is.EqualTo(typeof(Exception).Name));
            Assert.That(ex.ResponseStatus.ErrorCode, Is.EqualTo(typeof(Exception).Name));
            Assert.That(ex.ResponseStatus.Message, Is.EqualTo("Batch Throws"));
            Assert.That(ex.ResponseHeaders[HttpHeaders.XAutoBatchCompleted], Is.EqualTo("0"));
        }
    }
}